Micron Document
🎖️GitЯра🎖️

Commit c8d9795885f5f4edc710db10bc0d366dd72bccbc


Parents : 6cd7a32
Author : James Beeching <98554230+beecho01@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-08-17T17:43:07Z
Committer : GitHub <noreply@github.com>
Date : 2026-08-17T17:43:07Z

feat(node): label SNR quality on the Node Details signal row (#6703)

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

Changes

11 files changed, 104 insertions(+), 2 deletions(-)


Diff

diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponents.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponents.kt
index 5e66e454ef..d76933fbc7 100644
--- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponents.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponents.kt
@@ -93,6 +93,7 @@ internal fun InfoItem(
icon: ImageVector,
modifier: Modifier = Modifier,
valueStyle: TextStyle = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
+ valueColor: Color = MaterialTheme.colorScheme.onSurface,
iconTint: Color = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f),
iconSize: Dp = 14.dp,
onClick: (() -> Unit)? = null,
@@ -130,7 +131,7 @@ internal fun InfoItem(
)
}
Spacer(Modifier.height(4.dp))
- Text(text = value, style = valueStyle, color = MaterialTheme.colorScheme.onSurface)
+ Text(text = value, style = valueStyle, color = valueColor)
}
}

diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt
index c2d1f7160d..d71a399a4b 100644
--- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt
@@ -82,6 +82,7 @@ import org.meshtastic.core.resources.transport
import org.meshtastic.core.resources.uptime
import org.meshtastic.core.resources.user_id
import org.meshtastic.core.ui.component.SignedNodeDialog
+import org.meshtastic.core.ui.component.determineSignalQuality
import org.meshtastic.core.ui.component.transportInfo
import org.meshtastic.core.ui.icon.ArrowCircleUp
import org.meshtastic.core.ui.icon.DeviceNumbers
@@ -98,6 +99,7 @@ import org.meshtastic.core.ui.icon.Snr
import org.meshtastic.core.ui.icon.Verified
import org.meshtastic.core.ui.icon.role
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
+import org.meshtastic.core.ui.util.LocalModemPreset
import org.meshtastic.core.ui.util.createClipEntry
import org.meshtastic.core.ui.util.formatAgo
import org.meshtastic.proto.MeshPacket.TransportMechanism
@@ -292,9 +294,12 @@ private fun SignalRow(node: Node) {
Row(modifier = Modifier.fillMaxWidth()) {
val snr = node.snrOrNull
if (snr != null) {
+ val quality = determineSignalQuality(snr, LocalModemPreset.current)
+ // Value-before-quality with " · " matches the node-list signal pill in SignalInfo.kt.
InfoItem(
label = stringResource(Res.string.snr),
- value = MetricFormatter.snr(snr),
+ value = "${MetricFormatter.snr(snr)} · ${stringResource(quality.nameRes)}",
+ valueColor = quality.color(),
icon = MeshtasticIcons.Snr,
modifier = Modifier.weight(1f),
)
@@ -303,6 +308,7 @@ private fun SignalRow(node: Node) {
}
val rssi = node.rssiOrNull
if (rssi != null) {
+ // No quality word here: RSSI alone can't be rated without the noise floor - see determineSignalQuality.
InfoItem(
label = stringResource(Res.string.rssi),
value = MetricFormatter.rssi(rssi),

diff --git a/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/component/SignalRowQualityLabelTest.kt b/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/component/SignalRowQualityLabelTest.kt
new file mode 100644
index 0000000000..f2d4816446
--- /dev/null
+++ b/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/component/SignalRowQualityLabelTest.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.meshtastic.feature.node.component
+
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.test.ComposeUiTest
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.v2.runComposeUiTest
+import org.jetbrains.compose.resources.StringResource
+import org.meshtastic.core.common.util.MetricFormatter
+import org.meshtastic.core.model.Node
+import org.meshtastic.core.resources.Res
+import org.meshtastic.core.resources.bad
+import org.meshtastic.core.resources.fair
+import org.meshtastic.core.resources.getString
+import org.meshtastic.core.resources.good
+import org.meshtastic.core.resources.none_quality
+import org.meshtastic.core.resources.snr
+import org.meshtastic.core.ui.theme.AppTheme
+import org.meshtastic.core.ui.util.LocalModemPreset
+import org.meshtastic.proto.Config.LoRaConfig.ModemPreset
+import kotlin.test.Test
+
+/**
+ * Node Details' SignalRow reuses the already-tested [determineSignalQuality] to label SNR (see
+ * [LoraSignalIndicatorTest] for the underlying threshold coverage). RSSI intentionally keeps showing the raw value
+ * only - RSSI alone cannot indicate quality without the noise floor.
+ */
+@OptIn(ExperimentalTestApi::class)
+class SignalRowQualityLabelTest {
+
+ // Boundary values relative to ModemPreset.LONG_FAST's -17.5 dB demod floor, mirroring LoraSignalIndicatorTest.
+ private val preset = ModemPreset.LONG_FAST
+
+ @Test
+ fun signalRow_labelsGoodSnr() = runComposeUiTest {
+ assertSnrLabel(snr = -17f, expectedQualityRes = Res.string.good)
+ }
+
+ @Test
+ fun signalRow_labelsFairSnr() = runComposeUiTest {
+ assertSnrLabel(snr = -17.5f, expectedQualityRes = Res.string.fair)
+ }
+
+ @Test
+ fun signalRow_labelsBadSnr() = runComposeUiTest { assertSnrLabel(snr = -23f, expectedQualityRes = Res.string.bad) }
+
+ @Test
+ fun signalRow_labelsNoneSnr() = runComposeUiTest {
+ assertSnrLabel(snr = -30f, expectedQualityRes = Res.string.none_quality)
+ }
+
+ @Test
+ fun signalRow_rssiShowsOnlyTheRawValue() = runComposeUiTest {
+ setNodeDetails(Node(num = 1, snr = -17f, rssi = -100, hopsAway = 0))
+ onNodeWithText(MetricFormatter.rssi(-100)).assertExists()
+ }
+
+ @Test
+ fun signalRow_hidesWhenNodeHasNoSnrReading() = runComposeUiTest {
+ // hopsAway = 0 so SignalRow itself renders; default snr is the SNR_UNSET sentinel -> snrOrNull is null.
+ setNodeDetails(Node(num = 1, hopsAway = 0))
+ onNodeWithText(getString(Res.string.snr)).assertDoesNotExist()
+ }
+
+ private fun ComposeUiTest.assertSnrLabel(snr: Float, expectedQualityRes: StringResource) {
+ // hopsAway = 0 - matches MainNodeDetails' guard for rendering SignalRow at all.
+ setNodeDetails(Node(num = 1, snr = snr, rssi = -100, hopsAway = 0))
+ // Value-before-quality order matches the node-list signal pill in SignalInfo.kt.
+ val expected = "${MetricFormatter.snr(snr)} · ${getString(expectedQualityRes)}"
+ onNodeWithText(expected).assertExists()
+ }
+
+ private fun ComposeUiTest.setNodeDetails(node: Node) = setContent {
+ CompositionLocalProvider(LocalModemPreset provides preset) {
+ AppTheme { Surface { NodeDetailsSection(node = node) } }
+ }
+ }
+}

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Dark_d19fbf1f_0.png
index 110c886b73..d330b168fe 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Light_b29dc7a7_0.png
index 4077ee2bd9..10a45d1c5d 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Dark_d19fbf1f_0.png
index dba88945f3..75ac803b58 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Light_b29dc7a7_0.png
index c02399c867..2e32ca0cf4 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionSigned_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionSigned_Dark_d19fbf1f_0.png
index 5dcaa88093..145df97dc7 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionSigned_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionSigned_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionSigned_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionSigned_Light_b29dc7a7_0.png
index c58936f921..4c76a60aa0 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionSigned_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionSigned_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Dark_d19fbf1f_0.png
index 632f6bcc6b..d91ce72352 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Light_b29dc7a7_0.png
index 1b2748a520..87ea7e72ca 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Light_b29dc7a7_0.png differ

Served by rngit 1.5.0 - Generated in 0.22s